package org.mathig.poker.table;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mathig.poker.Action;
import org.mathig.poker.AfterBetActions;
import org.mathig.poker.AfterNothingActions;
import org.mathig.poker.Card;
import org.mathig.poker.Context;
import org.mathig.poker.Player;
import org.mathig.poker.PlayerListener;
import org.mathig.poker.PlayerWrapper;
import org.mathig.poker.StakesConfig;
import org.mathig.poker.Step;
import org.mathig.poker.action.AfterBetActionsImpl;
import org.mathig.poker.action.AfterNothingActionsImpl;
import org.mathig.poker.hand.Hand;
import org.mathig.poker.util.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author mathig
 */
public class TableHand implements Context {

	private Logger logger = LoggerFactory.getLogger(getClass());

	private Table table;

	private StakesConfig blindConfig;

	private Deck deck;

	private Step step;

	private List<Card> board = new ArrayList<Card>();

	/** Players for this hand (not fold), order from SB to Dealer. */
	private List<PlayerWrapper> players = new ArrayList<PlayerWrapper>();

	private Map<PlayerWrapper, PlayerCards> playerHands = new HashMap<PlayerWrapper, PlayerCards>();

	private List<PlayerListener> playerListeners = new ArrayList<PlayerListener>();

	private PlayerAmount bets = new PlayerAmount();

	private PlayerAmount pot = new PlayerAmount();

	private PlayerAmount winnings = new PlayerAmount();

	/**
	 * @param table
	 * @param players
	 *            players[0] is SB, players[1] is BB
	 */
	public TableHand(Table table, List<PlayerWrapper> players, StakesConfig blindConfig) {
		this.table = table;
		this.deck = table.getDeck();
		this.blindConfig = blindConfig;

		// init players
		this.players = players;
		logger.debug("hand players {}", players);
		if (this.players.size() < 2)
			throw new IllegalArgumentException("At least 2 players are needed to start a hand");
		for (PlayerWrapper player : players) {
			playerHands.put(player, new PlayerCards());
		}
	}

	public void play() {
		init();
		preFlop();
		if (players.size() > 1)
			postFlop();
		dealPot();
		eliminateBrokenPlayers();
	}

	private void postFlop() {
		flop();
		if (players.size() == 1)
			return;

		turn();
		if (players.size() == 1)
			return;

		river();
		if (players.size() == 1)
			return;

		showDown();
	}

	/**
	 * Deal pot between winners.
	 * <p>
	 * Ex : p1 all-in 100, p2 raise 200, p3 all-in 120, p4 all-in 180.<br>
	 * p1 p2 & p3 all have the same pair/kicker. p4 has a lower pair.<br>
	 * <ul>
	 * <li>all-in 100 : pot#1 = 4*100, 3 winners -> 133 to p1 p2 p3
	 * <li>all-in 120-100 : pot#2 = 3*20, 2 winners -> 30 to p2 p3
	 * <li>all-in 180-120 : pot#3 = 2*60, 1 winner -> 120 to p2
	 * <li>all-in 200-180 : pot#4 = 1*20 -> p2
	 * </ul>
	 */
	private void dealPot() {
		if (players.size() == 1) {
			// all fold, one winner for the whole pot
			PlayerWrapper winner = players.get(0);
			logger.info("{} is alone and wins the whole pot {}", winner, pot.getTotal());
			winnings.add(winner, pot.getTotal());
		} else {
			dealPotBetweenWinners();
		}
		moveWinningsToStacks();
	}

	private void dealPotBetweenWinners() {
		logger.debug("deal pot {} between {}...", pot, players);
		logger.debug("stacks {}", table.getStacks());

		// while pot is not fully dealed
		while (pot.getTotal() > 0) {
			logger.debug("remaining pot = {}", pot);

			// get remaining players (not fold and still has bet in pot)
			List<PlayerWrapper> winners = getPlayersWithBestHand(players);
			logger.debug("pot winners = {}", winners);

			// get min (all-in) of winners
			int minBet = pot.getMin(winners);
			logger.debug("min bet in pot = {}", minBet);

			List<PlayerWrapper> potPlayers = new ArrayList<PlayerWrapper>();
			for (PlayerWrapper player : players) {
				if (pot.get(player) >= minBet) {
					// player can pay this pot
					potPlayers.add(player);
				}
			}
			logger.debug("pot players = {}", potPlayers);
			int winnings = pot.removeAll(minBet);

			// remove players with no more part in pot
			List<PlayerWrapper> players2 = new ArrayList<PlayerWrapper>(players);
			for (PlayerWrapper player : players2)
				if (pot.get(player) == 0)
					players.remove(player);

			dealAmountBetween(winnings, winners);
		}
	}

	private void moveWinningsToStacks() {
		// winnings -> stack
		for (PlayerWrapper player : winnings.getPlayers()) {
			int amount = winnings.get(player);
			if (amount > 0) {
				int oldStack = table.getStack(player);
				table.getStacks().add(player, amount);
				logger.info("{} wins {}, stack {} -> {}", new Object[] { player, amount, oldStack, table.getStack(player) });
			}
		}
	}

	private void dealAmountBetween(int amount, List<PlayerWrapper> winners) {
		logger.debug("deal {} between {}", amount, winners);
		int winningsByPlayer = amount / winners.size();

		// deal gain to winners
		for (PlayerWrapper winner : winners) {
			int old = winnings.get(winner);
			winnings.add(winner, winningsByPlayer);
			logger.debug("{} wins {}, winnings {} -> {}", new Object[] { winner, winningsByPlayer, old, winnings.get(winner) });
		}

		// deal randomly crumbs
		int crumbs = amount % winners.size(); // les miettes
		if (crumbs > 0) {
			PlayerWrapper crumbsPlayer = winners.get(RandomUtils.random.nextInt(winners.size()));
			winnings.add(crumbsPlayer, crumbs);
			logger.debug("{} (choosed randomly) wins crumbs {}", crumbsPlayer, crumbs);
		}
	}

	private List<PlayerWrapper> getPlayersWithBestHand(List<PlayerWrapper> somePlayers) {
		if (somePlayers.isEmpty()) // should never happen
			throw new IllegalArgumentException("no players for this pot !");

		List<PlayerWrapper> bestHandPlayers = new ArrayList<PlayerWrapper>();

		if (somePlayers.size() == 1) {
			logger.debug("{} is the only winner", somePlayers.get(0));
			bestHandPlayers.add(somePlayers.get(0));
			return bestHandPlayers;
		}

		logger.debug("players hands : {}", playerHands);

		Hand bestPokerHand = null;

		for (PlayerWrapper player : somePlayers) {
			Hand pokerHand = playerHands.get(player).getPokerHand();
			if (pokerHand == null)
				continue;
			if (bestPokerHand == null) {
				// first player -> default best hand
				bestPokerHand = pokerHand;
				bestHandPlayers.add(player);
			} else {
				int compare = pokerHand.compareTo(bestPokerHand);
				if (compare == 0)
					bestHandPlayers.add(player);
				if (compare > 0) {
					// new best hand
					bestPokerHand = pokerHand;
					bestHandPlayers.clear();
					bestHandPlayers.add(player);
				}
			}
		}

		logger.debug("best hand is {}", bestPokerHand);
		return bestHandPlayers;
	}

	private void eliminateBrokenPlayers() {
		List<PlayerWrapper> players2 = new ArrayList<PlayerWrapper>(table.getPlayers());
		for (PlayerWrapper player : players2) {
			if (table.getStack(player) == 0) {
				table.eliminate(player);
			}
		}
	}

	public void init() {
		if (blindConfig.getAnte() > 0)
			betAntes();
		betBlinds();
	}

	/** Under The Gun. */
	private static final int UTG = 2;

	private void logStep(String step) {
		String s = table.handString() + " : " + step;
		if (board.size() > 0)
			s += " : " + board;
		logger.info(s);
	}

	/**
	 * Deal 2 x 1 cards to each player.
	 */
	private void preFlop() {
		logStep("pre-flop");
		step = Step.PREFLOP;
		dealOneCard();
		dealOneCard();
		for (PlayerWrapper player : players) {
			Card[] cards = playerHands.get(player).getCards().toArray(new Card[2]);
			player.getPlayer().receive(this, cards);
		}
		int utgIndex = players.size() > 2 ? UTG : 0;// at two dealer is also UTG
		logger.debug("{} is Under The Gun", players.get(utgIndex));
		askAllPlayers(true);
	}

	/**
	 * @param orderedPlayers
	 *            because order differs between preFlop and flop
	 */
	private void askAllPlayers(boolean startUTG) {
		if (playersNotBroken() < 2) {
			logger.debug("some players did all-in, players show their hand.");
			return;
		}
		PlayerWrapper sbPlayer = players.get(0);
		PlayerWrapper bbPlayer = players.get(1);
		do {
			preparePlayerListeners();

			List<PlayerWrapper> players2 = new ArrayList<PlayerWrapper>();
			players2.addAll(players);
			if (startUTG)
				moveBlindsAtEnd(players2, sbPlayer, bbPlayer);

			for (PlayerWrapper player : players2)
				if (players.size() > 1) // maybe all previous players fold
					ask(player);
		} while (players.size() > 1 && !allBetEquals());

		putBetsIntoPot();
	}

	private void preparePlayerListeners() {
		playerListeners = new ArrayList<PlayerListener>();
		for (PlayerWrapper player : players) {
			// a player may want to listen others
			if (player instanceof PlayerListener)
				playerListeners.add((PlayerListener) player);
		}
	}

	private void putBetsIntoPot() {
		for (PlayerWrapper player : bets.getPlayers()) {
			pot.add(player, bets.get(player));
		}
	}

	/**
	 * Move - if present - SB and BB players at the end of the list.
	 * 
	 * @param list
	 * @param sbPlayer
	 * @param bbPlayer
	 */
	private void moveBlindsAtEnd(List<PlayerWrapper> list, PlayerWrapper sbPlayer, PlayerWrapper bbPlayer) {
		if (list.remove(sbPlayer))
			list.add(sbPlayer);
		if (list.remove(bbPlayer))
			list.add(bbPlayer);
	}

	private AskingPlayer askingPlayer = new AskPlayer();

	private void ask(PlayerWrapper player) {
		if (table.getStack(player) > 0) {
			Action action = null;
			logger.info("{} bet {} < bet {}", new Object[] { player, bets.get(player), bets.getMax() });
			if (bets.get(player) < bets.getMax()) {
				AfterBetActions actions = new AfterBetActionsImpl(player, this);
				action = askingPlayer.ask(player, this, actions);
			} else {
				AfterNothingActions actions = new AfterNothingActionsImpl(player, this);
				action = askingPlayer.ask(player, this, actions);
			}
			action.execute();
			logger.debug("bet {} = {}", player, bets.get(player));

		}
	}

	private int playersNotBroken() {
		int count = 0;
		for (PlayerWrapper player : players)
			if (table.getStack(player) > 0)
				count++;
		return count;
	}

	/**
	 * Returns true when all hands players (not fold) called to the highest bet or all-in.
	 * 
	 * @return
	 */
	private boolean allBetEquals() {
		int bet = getBet();
		for (PlayerWrapper player : players) {
			if (bets.get(player) < bet && table.getStack(player) > 0)
				return false;
		}
		return true;
	}

	private void dealOneCard() {
		for (PlayerWrapper player : players)
			getHand(player).add(deck.next());
	}

	private void burnOneCard() {
		logger.debug("one card burned");
		deck.next();
	}

	private void flop() {
		step = Step.FLOP;
		burnOneCard();
		for (int i = 0; i < 3; i++)
			board.add(deck.next());

		logStep("flop");
		askAllPlayers(false);
	}

	private void turn() {
		step = Step.TURN;
		burnOneCard();
		board.add(deck.next());
		logStep("turn");
		askAllPlayers(false);
	}

	private void river() {
		step = Step.RIVER;
		burnOneCard();
		board.add(deck.next());
		logStep("river");
		askAllPlayers(false);
	}

	/**
	 * Final phase of a round.<br>
	 * Remaining players show down their cards.
	 * <p>
	 * Attacker must begin showing down its cards.<br>
	 * Then others followers has choice to show or fold (so he lose).
	 */
	private void showDown() {
		logger.info("showdown !");
		// Map<Player>
		for (PlayerWrapper player : players) {
			PlayerCards hand = playerHands.get(player);
			hand.compute(board);

			// TODO aggressor show first, others can muck
			logger.info("{} has {}", player, hand.getPokerHand());
		}

		// TODO players.remove(not best hand !)
	}

	private void betBlinds() {
		PlayerWrapper sbPlayer = players.get(0);
		addToBet(sbPlayer, blindConfig.getSmallBlind());
		logger.info("{} pay Small Blind {}", sbPlayer, blindConfig.getSmallBlind());

		PlayerWrapper bbPlayer = players.get(1);
		addToBet(bbPlayer, blindConfig.getBigBlind());
		logger.info("{} pay Big Blind {}", bbPlayer, blindConfig.getBigBlind());
	}

	private void betAntes() {
		for (PlayerWrapper player : players)
			addToBet(player, blindConfig.getAnte());
		logger.info("All players pay ante {}", blindConfig.getAnte());
	}

	private void addToBet(PlayerWrapper player, int amount) {
		if (amount > table.getStack(player)) {
			amount = table.getStack(player);
			logger.info("{} can't pay {} he all-in !", player, amount);
		}
		table.getStacks().remove(player, amount);
		bets.add(player, amount);
	}

	public int getPot() {
		return pot.getTotal();
	}

	public int getPotWithBets() {
		return pot.getTotal() + bets.getTotal();
	}

	public PlayerAmount getBets() {
		return bets;
	}

	public PlayerCards getHand(PlayerWrapper player) {
		return playerHands.get(player);
	}

	public List<Player> getPlayers() {
		List<Player> list = new ArrayList<Player>();
		for (PlayerWrapper player : players)
			list.add(player.getPlayer());
		return list;
	}

	public Table getTable() {
		return table;
	}

	public Step getStep() {
		return step;
	}

	public List<Card> getBoard() {
		return board;
	}

	public int getAnte() {
		return blindConfig.getAnte();
	}

	public int getBet() {
		return bets.getMax();
	}

	public int getBet(PlayerWrapper player) {
		return bets.get(player);
	}

	public int getBet(Player player) {
		for (PlayerWrapper playerWrapper : players)
			if (playerWrapper.getPlayer() == player)
				return bets.get(playerWrapper);
		throw new IllegalArgumentException(player + " not found");
	}

	public int getBigBlind() {
		return blindConfig.getBigBlind();
	}

	public int getSmallBlind() {
		return blindConfig.getSmallBlind();
	}

	public int getStack(PlayerWrapper player) {
		return table.getStacks().get(player);
	}

	public int getStack(Player player) {
		for (PlayerWrapper playerWrapper : players)
			if (playerWrapper.getPlayer() == player)
				return table.getStacks().get(playerWrapper);
		throw new IllegalArgumentException(player + " not found");
	}

	public List<PlayerListener> getPlayerListeners() {
		return playerListeners;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (PlayerWrapper player : players) {
			if (sb.length() > 0)
				sb.append(", ");
			sb.append(player + "=" + bets.get(player) + "/" + table.getStack(player));

		}
		return sb.toString();
	}

	public void fold(PlayerWrapper player) {
		players.remove(player);
	}

}
